package com.tsd.core.utils;

import com.alibaba.fastjson.JSON;
import com.tsd.core.annotation.GenSnQue;
import com.tsd.core.annotation.RepeatSubmit;
import com.tsd.core.vo.HlpException;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 加锁工具类
 * 使用方法
 * 方法1、对某个字符串进行加锁，获得锁之后才能操作。例如对某个记录审核
 * 在调用开始时调用getLock(str)，参数str是处理的对象标识
 * 通过获取锁的方式可以避免同一个用户基于某个记录重复提交而出现多条记录的情况（例如根据销售订单生成采购清单）
 * ，但是无法避免新建多条重复记录的情况（界面录入一次，多次提交，例如：客户询价），此时情况，可以采用提交前从服务器获取token，提交时检验token是否被使用，如果被使用，则认为已提交，不再处理
 * try {
 * if (!lock.tryLock()) {
 * System.out.println("无啊");
 * } else {
 * System.out.println("有");
 * }
 * } finally {
 * lock.unlock(); // 释放锁
 * }
 * 方法2、指令token法，每次指令使用token，使用之后将token失效，防止重复提交。此方法需要改造前端。
 * 方法3、加锁+限制提交时间，当获得锁并提交成功后，将参数的has值作为key存入dupMdMap中，后续的提交则检查此dupMdMap是否存在未过期的key
 *
 * @author Hillpool
 */
public class LockUtil {
    /**
     * HttpRequest header的参数名
     */
    public static final String HEADER_NAME_CMD_TOKEN = "cmd_token";

    /**
     * 发送的时间间隔（毫秒）
     */
    public static final long SUBMIT_INTERVAL = 60 * 1000;
    /**
     * 等待锁超时秒数
     */
    public static final long LOCK_WAITING_TIME_SECONDS = 50L;

    static ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();
    /**
     * 指令Token令牌：使用token令牌可以有效地防止重复提交。
     * 当客户端在填写数据时，先调用getSubmitToken获取一个uuid（该token不能在提交前获取）。
     * 然后，客户端在每次提交表单时都需要将这个token一起提交（使用header的cmd_token），业务方法调用hasToken检查这个token是否有效，
     * 如果有效，则可以处理请求，同时调用removeToken将token移除。如果token无效，则表明该表单已经提交过了。
     */
    static ConcurrentHashMap<String, Long> cmdTokenMap = new ConcurrentHashMap<>();
    /**
     * 重复提交控制
     * 处理成功后，将参数的md值加入到dupMdMap中
     * 下一个提交参数提交时，检查dupMdMap中是否存在，如果存在则返回“重复提交”
     * 设置一个检查时间，当检查时间间隔到时对过期的md值移除
     */
    static ConcurrentHashMap<String, Long> dupMdMap = new ConcurrentHashMap<>();

    static public Lock getLock(String s) {
        return locks.computeIfAbsent(s, k -> new ReentrantLock());
    }

    /**
     * @return
     */
    public static ConcurrentHashMap<String, Long> getCmdTokenMap() {
        return cmdTokenMap;
    }

    /**
     * 根据request的header的cmd_token，检查是否有效
     *
     * @param request
     * @return
     */
    public static boolean cmdTokenValid(HttpServletRequest request) {
        String token = request.getHeader(HEADER_NAME_CMD_TOKEN);
        return cmdTokenMap.containsKey(token);
    }

    /**
     * 判断是否有token，如果没有，则标识已经处理过，不能再处理
     *
     * @param token
     * @return
     */
    public static boolean cmdTokenValid(String token) {
        return token != null && cmdTokenMap.containsKey(token);
    }

    /**
     * 移除token，使用之后移除
     *
     * @param token
     */
    public static void removeCmdToken(String token) {
        cmdTokenMap.remove(token);
    }

    /**
     * 根据request内的cmd_token移除
     *
     * @param request
     */
    public static void removeCmdToken(HttpServletRequest request) {
        String token = request.getHeader(HEADER_NAME_CMD_TOKEN);
        cmdTokenMap.remove(token);
    }

    /**
     * 判单是否重复提交
     *
     * @param md5
     * @return
     */
    public static boolean isDup(String md5) {
        if (md5 == null) {
            return false;
        }
        Long t = dupMdMap.get(md5);
        return t != null && System.currentTimeMillis() - t < 0;
    }

    /**
     * 获取剩余时间
     *
     * @param md5
     * @return
     */
    public static long getDupRemainderTime(String md5) {
        if (md5 == null) {
            return 0;
        }
        Long time = dupMdMap.get(md5);
        if (time == null) {
            return 0;
        }
        return (time - System.currentTimeMillis()) / 1000;
    }

    static long lastCheckTime = 0;

    /**
     * 将key加入到缓存中
     *
     * @param key
     */
    public static void putKeyExpired(String key, long expiredMs) {
        if (key == null) {
            return;
        }
        dupMdMap.put(key, System.currentTimeMillis() + expiredMs);
        if (System.currentTimeMillis() - lastCheckTime > SUBMIT_INTERVAL) {
            lastCheckTime = System.currentTimeMillis();
            removeExpireKey();
        }
    }

    /**
     * 移除key
     */
    private static void removeKey(String key) {
        dupMdMap.remove(key);
    }

    /**
     * 移除已过期的Id
     */
    private static void removeExpireKey() {
        Set<String> keys = dupMdMap.keySet();
        for (String k : keys) {
            Long l = dupMdMap.get(k);
            if (System.currentTimeMillis() - l > 0) {
                dupMdMap.remove(k);
            }
        }
    }

    public static String getLockParamsKey(HttpServletRequest request, RepeatSubmit repeatSubmit) throws Exception {
        String cmdToken = request.getHeader(HEADER_NAME_CMD_TOKEN);
        Map<String, Object> params = RequestUtils.getReqParams(request);
        String paramsMd5;
        if (RepeatSubmit.Type.MOBILE.equals(repeatSubmit.limitType())) {
            paramsMd5 = HlpUtils.getString(params, repeatSubmit.mobileKey());
            if (HlpUtils.isEmpty(paramsMd5)) {
                throw new HlpException("缺少参数：mobile");
            }
        } else {
            paramsMd5 = HlpUtils.doMD(JSON.toJSONString(params), HlpUtils.MD5);
        }
        if (!HlpUtils.isEmpty(cmdToken)) {
            paramsMd5 = HlpUtils.doMD(paramsMd5 + cmdToken, HlpUtils.MD5);
        }
        return paramsMd5;
    }

    /**
     * 生成序号加锁的key
     *
     * @param request
     * @param genSnQue
     * @return
     * @throws Exception
     */
    public static String getGenSnKey(HttpServletRequest request, GenSnQue genSnQue) throws Exception {
        Map<String, Object> params = RequestUtils.getParams(request);
        StringBuilder buf = new StringBuilder();
        buf.append(genSnQue.ruleCode());
        String cn = genSnQue.clientFieldName();
        if (!HlpUtils.isEmpty(cn)) {
            buf.append(HlpUtils.getString(params, cn));
        }
        String sfn = genSnQue.supplierFieldName();
        if (!HlpUtils.isEmpty(sfn)) {
            buf.append(HlpUtils.getString(params, sfn));
        }
        return buf.toString();
    }
}
